/**
 * @author Jonathan Jung, Timothy Hong, Ronen Adam
 */

import java.net.DatagramSocket;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;


public class RouterEngine 
{
   // private LinkedList <Port> portList;
    
    private HashMap <String, Port> portMap;
    private ConcurrentLinkedQueue <EthernetFrame> receivedQueue;
    private boolean running;
    private RoutingTable table;
    
    public RouterEngine()
    {
    	//portList = new LinkedList<Port>();
    	System.out.println("entry into router engine const");
    	portMap = new HashMap<String, Port>();
        receivedQueue = new  ConcurrentLinkedQueue <EthernetFrame>();
        System.out.println("**");
        running = false;
    	table = new RoutingTable();
    	
    	System.out.println("***");
    }
    
    
//    public void run() throws InstantiationException, SocketException
//    {
//    	running = true;
//    	while(running == true)
//    	{
//    	    route();
//    	}
//    }
    
    
    public void addToPortMap(short portNum, String virtualIP, int subnet, int mtu)
    {
        Port port;
        
        try {
                port = new Port(portNum, virtualIP, subnet, mtu, this);
                System.out.println("port instantiated");
                port.start();
                System.out.println("port thread running");
                portMap.put(port.getVirtualIP(), port);
        } catch (UnknownHostException e) {
                e.printStackTrace();
        } catch (SocketException e) {
                e.printStackTrace();
        }
    }
    
    public void delFromPortMap(short portNum)
    {
    	portMap.remove(findPort(portNum).getVirtualIP());
    }

    public void connPort(short locPort, String realIP, short remPort) throws UnknownHostException
    {
    	Port temp = findPort(locPort);
		if (temp != null)
			temp.connect(remPort, realIP);
		else
			System.out.println("No port of that number exists");
    }
    
    public void disconPort(short locPort)
    {
    	Port temp = findPort(locPort);
		if (temp != null)
			temp.close();
		else
			System.out.println("No port of that number exists");
    }
    
    public void addRoute(String netIP, int subnet, String virIP)
    {
    	table.add(netIP, subnet, virIP);
    }
    
    public void delRoute(String netIP, int subnet, String virIP)
    {
    	table.delete(netIP);
    }
    
    public void send(String srcIP, String desIP, short ID, int N) throws InstantiationException, SocketException
    {

    	String aplha = "abcdefghijklmnopqrstuvwxyz";
    	
    	byte [] alphabet = aplha.getBytes();
    	byte [] message = new byte [N];
    	
    	for(int i= 0; i<N; i++)
    		message[i] = alphabet[i%26];
    	
    	  	//TODO: need a simpler constructor for sending
    	IPv4Datagram toSend = new IPv4Datagram(ID, true, true, (short)0, 100, true, srcIP, desIP, message);
    	route(toSend);
    }
    
    public ConcurrentLinkedQueue <EthernetFrame> getReceivedQ()
    {
        return receivedQueue;
    }
    

    
    public synchronized void route() throws InstantiationException, SocketException
    {
        if (!receivedQueue.isEmpty())
        {
            IPv4Datagram sausage = new IPv4Datagram(receivedQueue.poll().getFramePayload(), true);
            
            //Find appropriate gateway using routing table 
            String destinationIP = sausage.getDestinationIP();         
            String gatewayIP = table.getEndpoint(destinationIP);
            Port outGoing = portMap.get(gatewayIP);
           
            //If 
            if (outGoing.getMTU() < sausage.getLength())
            {
                cutSausage(sausage);
            }
            else
            {
                outGoing.transmit(sausage);
            }
        
        }
    }

    //overloaded method for send command
    public synchronized void route(IPv4Datagram sausage) throws InstantiationException, SocketException
    {
    	//Find appropriate gateway using routing table 
    	String destinationIP = sausage.getDestinationIP();         
    	String gatewayIP = table.getEndpoint(destinationIP);
    	Port outGoing = null;
  
    	for (Entry<String, Port> entry : portMap.entrySet())
    	{
    		Port temp = entry.getValue();
    		
    		temp.transmit(sausage);
    		
    	}
    	
    	
    	if (outGoing == null)
    	{
    		System.out.println("packet dropped");
    	
    	}
   
    	else if (outGoing.getMTU() < sausage.getLength())
    	{
    		cutSausage(sausage);
    	}
    	else
    	{
    		outGoing.transmit(sausage);
    	}
    }
    
    
    //:TODO actually make the cutter
    private ArrayList<IPv4Datagram> cutSausage(IPv4Datagram sausage) throws InstantiationException
    {
        IPv4Datagram sausageHolder = sausage;
        
        //:TODO cut stuff here
        
        ArrayList<IPv4Datagram> sausageBits = new ArrayList<IPv4Datagram>();
        
		return sausageBits; 
    }

    private Port findPort(short portNum)
    {
    	for (Entry<String, Port> entry : portMap.entrySet())
    	{
    		Port temp = entry.getValue();
    		
    		if(temp.getPortNum() == portNum)
    		{
    			return portMap.get(temp.getVirtualIP());
    		}
    	}
    	
    	return null;
    }

    private String calculateIP(String address, int subnetValue) {
        // 1. Calculate the subnet mask.
        int subnetMask = 0;

        if (subnetValue > 0) {
            subnetMask = 0x80000000; // '1' followed by zeros
            /*
             * Right-shifting the '1' will result in a one in each shifted
             * position. There is already a '1' in the mask so the subnet value
             * is '1' too many bits for an accurate shift. Subtract '1' to fix.
             */
            subnetMask = subnetMask >> (subnetValue - 1);
        }

        // 2. Calculate the IP using the subnet mask.

        // Isolate each octet from the IP string.
        String[] addressSlices = address.split("\\.");

        // Assemble an integer ip value from the octets.
        // Shift each octet to its appropriate position in a 32-bit value.
        int octet1 = Integer.valueOf(addressSlices[0]) << 24;
        int octet2 = Integer.valueOf(addressSlices[1]) << 16;
        int octet3 = Integer.valueOf(addressSlices[2]) << 8;
        int octet4 = Integer.valueOf(addressSlices[3]);

        int ipValue = octet1 + octet2 + octet3 + octet4;

        // 3. Perform the bitwise-and operation with the IP and the subnet mask.
        int newIpValue = ipValue & subnetMask;

        // 4. Extract the calculated octets.
        int octet1Mask = 0x00FFFFFF;
        int octet2Mask = 0xFF00FFFF;
        int octet3Mask = 0xFFFF00FF;
        int octet4Mask = 0xFFFFFF00;

        // Shift the octets back to individual 8-bit values.
        octet1 = ((newIpValue | octet1Mask) ^ octet1Mask) >> 24;
        /*
         * The first octet may have been corrupted by overflow if the original
         * value was greater than 127. The result of a value of '128' - '255'
         * can be corrected by adding '256' to return a positive 8-bit value.
         */
        if (octet1 < 0) {
            octet1 += 256;
        }

        octet2 = ((newIpValue | octet2Mask) ^ octet2Mask) >> 16;
        octet3 = ((newIpValue | octet3Mask) ^ octet3Mask) >> 8;
        octet4 = (newIpValue | octet4Mask) ^ octet4Mask;

        // 5. Assemble the calculated IP.
        StringBuilder sb = new StringBuilder();
        sb.append(octet1);
        sb.append(".");
        sb.append(octet2);
        sb.append(".");
        sb.append(octet3);
        sb.append(".");
        sb.append(octet4);
        sb.trimToSize();

        return sb.substring(0);
    }
}